﻿///<copyright>
/// Copyright © 2009 Stephen Styrchak. All Rights Reserved.
/// Portions copyright (C) Microsoft Corporation. All rights reserved.
///</copyright>
using System;
using System.Collections.Generic;
using System.ComponentModel;
using Ferpect.ComponentModel.Design;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.Graphics;
#if WINDOWS
using System.ComponentModel.Design;
#endif

namespace Ferpect.ComponentModel.Sprites
{
    [Description("Draws a Texture2D image.")]
#if WINDOWS
    [Designer(Designers.ParticlesSpriteDesigner, typeof(IDesigner))]
#endif
    public class ParticlesSprite : UpdatableSprite
    {
        private SpriteBatchInfo spriteBatchInfo;
        private Texture2D texture;
        private string textureName;

        /// <summary>
        /// The array of particles used by this system.
        /// </summary>
        private Particle[] particles;
        private Queue<int> freeParticles;

        // these two values control the order that particle systems are drawn in.
        // typically, particles that use additive blending should be drawn on top of
        // particles that use regular alpha blending. ParticleSystems should therefore
        // set their DrawOrder to the appropriate value in InitializeConstants, though
        // it is possible to use other values for more advanced effects.
        public const int AlphaBlendDrawOrder = 100;
        public const int AdditiveDrawOrder = 200;

        /// <summary>
        /// The maximum number of effects this particle system will be expected to draw at one time.
        /// </summary>
        private int howManyEffects;

        private int minNumParticles;
        private int maxNumParticles;
        private float minInitialSpeed;
        private float maxInitialSpeed;
        private float minAcceleration;
        private float maxAcceleration;
        private float minRotationSpeed;
        private float maxRotationSpeed;
        private float minLifetime;
        private float maxLifetime;
        private float minScale;
        private float maxScale;
        private BlendState blendState;

        public ParticlesSprite()
        {
            this.howManyEffects = 1;
            this.RelativeOrigin = RelativeLocation.MiddleCenter;
        }

        /// <summary>
        /// returns the number of particles that are available for a new effect.
        /// </summary>
        [Browsable(false)]
        public int FreeParticleCount
        {
            get { return freeParticles.Count; }
        }

        #region Properties

        [Category("Behavior")]
        [DefaultValue(1)]
        public int HowManyEffects
        {
            get { return howManyEffects; }
            set { howManyEffects = value; }
        }

        [Category("Appearance")]
        public int MinNumParticles
        {
            get { return minNumParticles; }
            set { minNumParticles = value; }
        }

        [Category("Appearance")]
        public int MaxNumParticles
        {
            get { return maxNumParticles; }
            set { maxNumParticles = value; }
        }

        [Category("Behavior")]
        public float MinInitialSpeed
        {
            get { return minInitialSpeed; }
            set { minInitialSpeed = value; }
        }

        [Category("Behavior")]
        public float MaxInitialSpeed
        {
            get { return maxInitialSpeed; }
            set { maxInitialSpeed = value; }
        }

        [Category("Behavior")]
        public float MinAcceleration
        {
            get { return minAcceleration; }
            set { minAcceleration = value; }
        }

        [Category("Behavior")]
        public float MaxAcceleration
        {
            get { return maxAcceleration; }
            set { maxAcceleration = value; }
        }

        [Category("Behavior")]
        public float MinRotationSpeed
        {
            get { return minRotationSpeed; }
            set { minRotationSpeed = value; }
        }

        [Category("Behavior")]
        public float MaxRotationSpeed
        {
            get { return maxRotationSpeed; }
            set { maxRotationSpeed = value; }
        }

        [Category("Behavior")]
        public float MinLifetime
        {
            get { return minLifetime; }
            set { minLifetime = value; }
        }

        [Category("Behavior")]
        public float MaxLifetime
        {
            get { return maxLifetime; }
            set { maxLifetime = value; }
        }

        [Category("Appearance")]
        public float MinScale
        {
            get { return minScale; }
            set { minScale = value; }
        }

        [Category("Appearance")]
        public float MaxScale
        {
            get { return maxScale; }
            set { maxScale = value; }
        }

        [Category("Appearance")]
        [RefreshContent()]
        public BlendState BlendState
        {
            get { return blendState; }
            set { blendState = value; }
        }

        [Category("Appearance")]
        [TypeConverter(TypeConverters.Texture2DConverter)]
        [RefreshContent()]
        public string TextureName
        {
            get
            {
                return this.textureName;
            }
            set
            {
                this.textureName = value;
            }
        }

        [Browsable(false)]
        [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
        public Texture2D Texture
        {
            get
            {
                return this.texture;
            }
        }

        #endregion

        /// <summary>
        /// AddParticles's job is to add an effect somewhere on the screen. If there 
        /// aren't enough particles in the freeParticles queue, it will use as many as 
        /// it can. This means that if there not enough particles available, calling
        /// AddParticles will have no effect.
        /// </summary>
        /// <param name="where">where the particle effect should be created</param>
        public void AddParticles(Vector2 where)
        {
            // the number of particles we want for this effect is a random number
            // somewhere between the two constants specified by the subclasses.
            int numParticles = Particle.Random.Next(minNumParticles, maxNumParticles);

            // create that many particles, if you can.
            for (int i = 0; i < numParticles && freeParticles.Count > 0; i++)
            {
                // grab a particle from the freeParticles queue, and Initialize it.
                int index = freeParticles.Dequeue();
                InitializeParticle(ref this.particles[index], where);
            }
        }

        /// <summary>
        /// InitializeParticle randomizes some properties for a particle, then
        /// calls initialize on it. It can be overriden by subclasses if they 
        /// want to modify the way particles are created. For example, 
        /// SmokePlumeParticleSystem overrides this function make all particles
        /// accelerate to the right, simulating wind.
        /// </summary>
        /// <param name="p">the particle to initialize</param>
        /// <param name="where">the position on the screen that the particle should be
        /// </param>
        protected virtual void InitializeParticle(ref Particle particle, Vector2 where)
        {
            // first, call PickRandomDirection to figure out which way the particle
            // will be moving. velocity and acceleration's values will come from this.
            Vector2 direction = PickRandomDirection();

            // Pick some random values for our particle.
            float velocity = this.minInitialSpeed + (float)Particle.Random.NextDouble() * (this.maxInitialSpeed - this.minInitialSpeed);
            float acceleration = this.minAcceleration + (float)Particle.Random.NextDouble() * (this.maxAcceleration - this.minAcceleration);
            float lifetime = this.minLifetime + (float)Particle.Random.NextDouble() * (this.maxLifetime - this.minLifetime);
            float scale = this.minScale + (float)Particle.Random.NextDouble() * (this.maxScale - this.minScale);
            float rotationSpeed = this.minRotationSpeed + (float)Particle.Random.NextDouble() * (this.maxRotationSpeed - this.minRotationSpeed);

            particle.Initialize(where, velocity * direction * this.Scale, acceleration * direction * this.Scale, lifetime, scale * this.Scale, rotationSpeed);
        }

        /// <summary>
        /// PickRandomDirection is used by InitializeParticles to decide which direction
        /// particles will move. The default implementation is a random vector in a
        /// circular pattern.
        /// </summary>
        protected virtual Vector2 PickRandomDirection()
        {
            float angle = (float)Particle.Random.NextDouble() * MathHelper.TwoPi;
            return new Vector2((float)Math.Cos(angle), (float)Math.Sin(angle));
        }

        public override void LoadContent(ContentManager content)
        {
            if (content == null) throw new ArgumentNullException("content");

            IGraphicsDeviceService graphicsDeviceService = (IGraphicsDeviceService)this.GetService(typeof(IGraphicsDeviceService));
            this.spriteBatchInfo = new SpriteBatchInfo(graphicsDeviceService.GraphicsDevice, this.BlendState);

            int countParticles = howManyEffects * maxNumParticles;
            this.particles = new Particle[countParticles];
            this.freeParticles = new Queue<int>(countParticles);

            for (int i = 0; i < particles.Length; i++)
            {
                this.particles[i] = new Particle();
                this.freeParticles.Enqueue(i);
            }

            if (!String.IsNullOrEmpty(this.textureName))
            {
                this.texture = content.Load<Texture2D>(this.textureName);
            }
            this.ResetRelativeOrigin();
        }

        public override void UnloadContent()
        {
            this.texture = null;
        }

        public override void Draw(SpriteBatchStack spriteBatchStack)
        {
            if (spriteBatchStack == null) throw new ArgumentNullException("spriteBatchStack");

            if (!this.Visible)
                return;

            // tell sprite batch to begin, using the spriteBlendMode specified in
            // initializeConstants
            spriteBatchStack.Push(this.spriteBatchInfo);
            try
            {
                SpriteBatch spriteBatch = spriteBatchStack.Current;

                Color tint = this.Color;
                Vector2 textureOrigin = this.Origin;

                for (int i = 0; i < this.particles.Length; ++i)
                {
                    if (this.particles[i].Active)
                    {
                        this.particles[i].Draw(spriteBatch, this.Texture, ref tint, ref textureOrigin);
                    }
                }
            }
            finally
            {
                spriteBatchStack.Pop();
            }
        }

        public override Vector2 GetSize()
        {
            if (this.texture != null)
            {
                return new Vector2(this.texture.Width, this.texture.Height);
            }
            return new Vector2(64, 64);
        }

        public override void Initialize()
        {
        }

        public override void Update(GameTime gameTime)
        {
            // Calculate dt, the change in the since the last frame.
            // The particle updates will use this value.
            float dt = (float)gameTime.ElapsedGameTime.TotalSeconds;

            for (int i = 0; i < this.particles.Length; ++i)
            {
                if (this.particles[i].Active)
                {
                    this.particles[i].Update(dt);
                    // See if the update made the particle inactive.
                    if (!this.particles[i].Active)
                    {
                        this.freeParticles.Enqueue(i);
                    }
                }
            }
        }
    }
}
